/*
 * Copyright 2007, 2008 Tiberiumlabs
 *
 * This file is part of Tiberiumlabs Shards.
 *
 * Tiberiumlabs Shards is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tiberiumlabs Shards is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.tiberiumlabs.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 *
 * @param E type of object for queue.
 * @author <a href="paranoid.tiberiumlabs@gmail.com">Paranoid</a>
 */
public abstract class BackgroundWorker<E> {

    /**
     * Override this method to perform some task with each element you submitted.<br/>
     * Parameter is submitted here in order they were added to method.<br/>
     * If exceptions will occur - it will be processed in
     * <code>processException(E, Throwable)</code> method.<br/>
     * @param e next element from queue
     * @throws java.lang.Throwable any exception can be thrown and will be
     * processed in <code>processException(E, Throwable)</code> method.
     */
    public abstract void doInBackground(E e) throws Throwable;

    /**
     * Method to handle exception, thrown from <code>doInBackground(E)</code>.<br/>
     * This structure helps not to break working thread.<br/>
     * You can override this method to be empty if you want to ignore exceptions
     * or if you surrounded it with try-catch block inside <code>doInBackground(E)</code>.
     *
     * @param e element, which has exception
     * @param th exception, that throwed from <code>doInBackground(E)</code>.
     */
    public void processException(E e, Throwable th) {
        System.err.print("Exception with element: " + e + "; ");
        th.printStackTrace(System.err);
    }

    // <editor-fold defaultstate="collapsed" desc=" work related block ">

    private Thread worker;
    private boolean shutdown = false;

    /**
     * Method to start worker thread.<br/>
     * Worker is started with specified (or default) priority.<br/>
     * If <code>lazy</code> is set - worker will start only when first task will be submitted.<br/>
     * Default priority is <code>Thread.NORM_PRIORITY</code>.<br/>
     */
    public final void start() {
        this.worker = new Thread() {
            @Override
            public void run() {
                process();
            }
        };
        this.worker.setDaemon(daemon);
        this.worker.setPriority(priority);
        this.worker.setName(name);
        if (!lazy) {
            this.worker.start();
        }
    }

    public final void shutdown() {
        this.shutdown = true;
        notifyWorker();
    }

    public final void shutdownAndWait() {
        shutdown();
        if (worker != null) {
            try {
                worker.join();
            } catch (InterruptedException ex) {
            }
        }
    }

    public final List<E> shutdownNow() {
        synchronized (tasksList) {
            shutdown = true;
            List<E> result = new ArrayList<E>(tasksList.size());
            Iterator<E> taskIterator = tasksList.iterator();
            while (taskIterator.hasNext()) {
                result.add(taskIterator.next());
            }
            tasksList.clear();
            notifyWorker();
            worker.interrupt();
            return result;
        }
    }

    private final void notifyWorker() {
        if (lazy) {
            if (shutdown) {
                return;
            }
            lazy = false;
            this.worker.start();
            return;
        }
        synchronized (this) {
            notify();
        }
    }

    private final void process() {
        while (true) {
            if (tasksList.isEmpty()) {
                if (shutdown) {
                    return;
                }
                try {
                    synchronized (this) {
                        wait();
                    }
                } catch (InterruptedException ignore) {
                }
            }
            while (tasksList.size() > 0) {
                E e;
                synchronized (tasksList) {
                    e = tasksList.removeFirst();
                }
                try {
                    doInBackground(e);
                } catch (Throwable th) {
                    processException(e, th);
                }
            }
        }
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" list related block ">

    private final LinkedList<E> tasksList = new LinkedList<E>();

    /**
     * Executes <code>doInBackground</code> method on this element in the future.<br/>
     * Excecution will be in same order as elements were added.<br/>
     * If worker thread is not already working - it will be created and started.<br/>
     * If worker is already created - this task will pass to <code>doInBackground</code>
     * after all current tasks will be completed.<br/>
     * Once task is submitted to worker - it can be removed only by <code>purge</code> method.<br/>
     * @param e element to append to queue.
     */
    public final void add(E e) {
        if (shutdown || e == null) {
            return;
        }
        synchronized (tasksList) {
            tasksList.addLast(e);
        }
        notifyWorker();
    }

    /**
     * Executes <code>doInBackground</code> method on this elements in the future.<br/>
     * Excecution will be in same order as elements were added.<br/>
     * If worker thread is not already working - it will be created and started.<br/>
     * If worker is already created - this tasks will pass to <code>doInBackground</code>
     * after all current tasks will be completed.<br/>
     * <p>
     * First all elements from collection will be added to current queue, and then worker
     * thread will be notified of new tasks.
     * @param collection elements, that will be appended.
     */
    public final void addAll(Collection<? extends E> collection) {
        if (shutdown || collection == null || collection.isEmpty()) {
            return;
        }
        synchronized (tasksList) {
            tasksList.addAll(collection);
        }
        notifyWorker();
    }

    /**
     * Clears current tasks list, so worker will stop after current iteration is done.
     */
    public final void purge() {
        synchronized (tasksList) {
            tasksList.clear();
        }
    }
    
    /**
     * Clears current tasks list, so worker will stop after current iteration is done.
     */
    public final void clear() {
        purge();
    }

    /**
     * Removes the first occurrence of the specified element from list,
     * if it is present.  If list does not contain the element, it is
     * unchanged.
     * Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param e element to be removed from queue, if present
     * @return <tt>true</tt> if list contained the specified element
     */
    public final boolean remove(E e) {
        boolean result = false;
        synchronized (tasksList) {
            result = tasksList.remove(e);
        }
        return result;
    }

    /**
     * Returns current tasks list. This list is unmodifiable, so you
     * can't add or remove tasks from it.
     * @return current tasks list.
     */
    public final List<? extends E> getTasksList() {
        return tasksList;
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" thread-related params and properties ">

    private int priority = Thread.NORM_PRIORITY;
    private boolean daemon = false;
    private String name = getClass().getName();
    private boolean lazy = false;

    /**
     * Tests if worker thread is a daemon thread.
     * @return <code>true</code> if worker thread is a daemon thread;
     *          <code>false</code> otherwise.
     */
    public boolean isDaemon() {
        return daemon;
    }

    /**
     * Marks worker thread as either a daemon thread or a user thread. The
     * Java Virtual Machine exits when the only threads running are all
     * daemon threads.
     * <p>
     * This method must be called before the worker is started.
     * @param daemon if <code>true</code>, marks worker thread as a daemon thread.
     * @see     java.lang.Thread#setDaemon(boolean)
     */
    public void setDaemon(boolean daemon) {
        this.daemon = daemon;
    }

    /**
     * Returns name of worker thread.
     * @return name of worker thread.
     */
    public String getName() {
        return name;
    }

    /**
     * Changes the name of worker thread.
     * @param name the new name for worker thread.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Returns worker thread's priority.
     * @return worker thread's priority.
     */
    public int getPriority() {
        return priority;
    }

    /**
     * Changes the priority of worker thread.
     * @param priority priority to set worker thread to
     * @see     java.lang.Thread#setPriority(int)
     */
    public void setPriority(int priority) {
        this.priority = priority;
    }

    /**
     * Tests if this worker is lazy.
     * @return <code>true</code> if this worker is lazy, <code>false</code> otherwise.
     * @see #setLazy
     */
    public boolean isLazy() {
        return lazy;
    }

    /**
     * Set this worker as lazy worker.<br/>
     * if <code>true</code> - worker thread will be started only when first task
     * will be added. Otherwise - will be started at once when <code>start()</code>
     * invoked.
     * <p>
     * Default is <code>false</code>.
     * @param lazy
     * @see #start
     */
    public void setLazy(boolean lazy) {
        this.lazy = lazy;
    }

    // </editor-fold>

}
